package org.dicr.radius.dict;

import java.io.*;
import java.lang.ref.*;
import java.net.*;
import java.text.*;
import java.util.*;

import javax.xml.parsers.*;

import org.dicr.radius.*;
import org.slf4j.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;

/**
 * Implementation of {@link RadiusDictionary} XML data source.<br/>
 * Each vendor data cached in memory with {@link SoftReference}.<br/>
 * Default location of xml is {@link #URL_DEFAULT}.
 *
 * @author <a href="http://dicr.org/">Igor (Dicr) Tarasov</a> {@literal <develop@dicr.org>}
 * @version 141026
 */
public final class XMLDictionary extends AbstractDictionary {

	/** Default dictionary resource name */
	public static final String URL_DEFAULT = "dictionary.xml";

	/** Logger */
	private static final Logger log = LoggerFactory.getLogger(XMLDictionary.class);

	/**
	 * Constructor
	 *
	 * @param url resource of data
	 */
	private XMLDictionary(final URL url) {
		super(url);
	}

	/**
	 * Return existing or create new dictionary instance for given URL.
	 *
	 * @param url xml-resource address
	 * @return dictionary instance
	 */
	public static AbstractDictionary getInstance(final URL url) {
		if (url == null) throw new IllegalArgumentException("null dictionary");
		AbstractDictionary dict = null;
		synchronized (AbstractDictionary.INSTANCES) {
			final Reference<AbstractDictionary> ref = AbstractDictionary.INSTANCES.get(url);
			if (ref != null) dict = ref.get();
			if (dict == null) {
				dict = new XMLDictionary(url);
				AbstractDictionary.INSTANCES.put(url, new WeakReference<>(dict));
			}
		}
		return dict;
	}

	/**
	 * Return existing or create new dictionary instance for default url
	 *
	 * @return dictionary instance
	 * @throws FileNotFoundException if default resource file not found
	 * @see #URL_DEFAULT
	 */
	public static final AbstractDictionary getInstance() throws FileNotFoundException {
		final URL url = XMLDictionary.class.getResource(XMLDictionary.URL_DEFAULT);
		if (url == null) throw new FileNotFoundException(XMLDictionary.URL_DEFAULT);
		return XMLDictionary.getInstance(url);
	}

	/**
	 * Load Attributes from a dictionary file
	 */
	@Override
	public final Map<Integer, VendorDescriptor> loadData() throws IOException, ParseException {
		XMLDictionary.log.debug("loading XML data from: " + this.url);
		final Parser handler = new Parser();
		try {
			SAXParserFactory.newInstance().newSAXParser().parse(this.url.toString(), handler);
		} catch (Exception ex) {
			throw new ParseException(ex.getMessage(), 0);
		}
		return handler.vendors;
	}

	@Override
	public void exportData(OutputStream ou) throws IOException {
		if (ou == null) throw new IllegalArgumentException("null out");
		Map<Integer, VendorDescriptor> data = this.buildExportData();
		try (PrintStream out = new PrintStream(ou);) {
			out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
			out.println("<!DOCTYPE dictionary SYSTEM \"dictionary.dtd\">");
			out.println("<!-- Radius XML data exported " + DateFormat.getDateInstance().format(new Date()) + " by "
					+ this.getClass().getName() + " -->");
			out.println("<dictionary name=\"radius\">");
			int attrCounter = 0;
			for (Integer vcode : new TreeSet<>(data.keySet())) {
				VendorDescriptor vendor = data.get(vcode);
				out.printf("\t<vendor code=\"%d\" name=\"%s\"", vcode, vendor.name);
				if (vendor.attributes.isEmpty()) out.println("/>");
				else {
					out.println(">");
					for (AttDescriptor attr : new TreeSet<>(vendor.attributes.values())) {
						SortedSet<Map.Entry<String, Long>> values = new TreeSet<>(new Comparator<Map.Entry<String, Long>>() {
							@Override
							public int compare(Map.Entry<String, Long> e1, Map.Entry<String, Long> e2) {
								return e1.getValue().compareTo(e2.getValue());
							}
						});
						values.addAll(attr.listValues().entrySet());
						out.printf("\t\t<attribute code=\"%d\" name=\"%s\" type=\"%s\" encoding=\"%s\"", Integer.valueOf(attr.getCode()),
								attr.getName(), attr.getType().toString().toLowerCase(), (attr.getEncoding() != null ? attr.getEncoding() : ""));
						if (values.isEmpty()) out.println("/>");
						else {
							out.println(">");
							for (Map.Entry<String, Long> entry : values)
								out.printf("\t\t\t<value name=\"%s\" data=\"%d\"/>\n", entry.getKey(), entry.getValue());
							out.println("\t\t</attribute>");
						}
						attrCounter++;
					}
					out.println("\t</vendor>");
				}
			}
			out.println("</dictionary>");
			out.flush();
			XMLDictionary.log.debug("exported " + data.size() + " vendors, " + attrCounter + " attributes");
		}
		
	}

	/**
	 * XML Processor handler
	 */
	private static class Parser extends DefaultHandler {

		@SuppressWarnings("hiding")
		private final static Logger log = LoggerFactory.getLogger(Parser.class);

		private static final String E_DOCUMENT = "document";
		private static final String E_DICTIONARY = "dictionary";
		private static final String E_VENDOR = "vendor";
		private static final String E_ATTRIBUTE = "attribute";
		private static final String E_VALUE = "value";
		private static final String A_NAME = "name";
		private static final String A_CODE = "code";
		private static final String A_TYPE = "type";
		private static final String A_ENCODING = "encoding";
		private static final String A_DATA = "data";

		private String state = null;
		private Integer currVendor = null;
		private String currAttrib = null;
		protected Map<Integer, VendorDescriptor> vendors = null;

		/** Constructor */
		public Parser() {
			this.vendors = new HashMap<>();
			this.vendors.put(Integer.valueOf(0), new VendorDescriptor("RFC"));
		}

		@Override
		public final void startDocument() {
			this.state = Parser.E_DOCUMENT;
		}

		@Override
		public final void endDocument() {
			this.state = null;
		}

		@Override
		public final void startElement(final String uri, final String localName, final String qName, final Attributes attrs) {
			if (qName == null) throw new IllegalArgumentException("null qName");
			Integer code = null;
			String name = null;
			ValueType type = null;
			String enc = null;
			Long data = null;
			try {
				switch (qName.toLowerCase()) {
					case E_DICTIONARY:
						if (this.state != Parser.E_DOCUMENT) Parser.log.warn("illegal tag " + qName + " in " + this.state);
						this.state = Parser.E_DICTIONARY;
						this.currVendor = null;
						this.currAttrib = null;
						break;

					case E_VENDOR:
						if (this.state != Parser.E_DICTIONARY) Parser.log.warn("illegal tag " + qName + " in " + this.state);
						this.state = Parser.E_VENDOR;
						this.currVendor = null;
						this.currAttrib = null;
						for (int i = 0; i < attrs.getLength(); i++)
							switch (attrs.getQName(i)) {
								case A_CODE:
									code = Integer.valueOf(attrs.getValue(i));
									break;
								case A_NAME:
									name = attrs.getValue(i);
									break;
								default:
									Parser.log.warn("unknown vendor attribute '" + attrs.getQName(i) + "'");
							}
						if (name == null) throw new ParseException("no name attribute in vendor", 0);
						if (code == null) throw new ParseException("no code attribute in vendor", 0);
						if (code.intValue() < 0 || code.intValue() > AbstractDictionary.VENDOR_MAX) throw new ParseException(
								"illegal vendor attribute code=" + code, 0);
						this.currVendor = code;
						this.vendors.put(code, new VendorDescriptor(name));
						break;

					case E_ATTRIBUTE:
						if (this.state != Parser.E_VENDOR) Parser.log.warn("illegal attribute " + qName + " in " + this.state);
						this.state = Parser.E_ATTRIBUTE;
						this.currAttrib = null;
						for (int i = 0; i < attrs.getLength(); i++)
							switch (attrs.getQName(i)) {
								case A_CODE:
									code = Integer.valueOf(attrs.getValue(i));
									break;
								case A_NAME:
									name = attrs.getValue(i);
									break;
								case A_TYPE:
									type = ValueType.fromString(attrs.getValue(i));
									if (type == null) {
										Parser.log.warn("unknown attribute type=" + attrs.getValue(i));
										type = ValueType.OCTETS;
									}
									break;
								case A_ENCODING:
									enc = attrs.getValue(i);
									break;
								default:
									Parser.log.warn("unknown attribute '" + attrs.getQName(i) + "' in attribute element");
							}
						if (name == null) throw new ParseException("no attribute name", 0);
						if (code == null) throw new ParseException("no code in attribute " + name, 0);
						if (code.intValue() < 0
								|| (this.currVendor != null && this.currVendor.intValue() == AbstractDictionary.VENDOR_NONE && code
										.intValue() > AbstractDictionary.CODE_MAX)) throw new ParseException("invalid attribute code="
								+ code, 0);
						if (type == null) throw new ParseException("no type in attribute " + name, 0);
						if (this.currVendor != null) {
							this.currAttrib = name;
							this.vendors.get(this.currVendor).attributes.put(name,
									new AttDescriptor(this.currVendor.intValue(), code.intValue(), name, type, enc));
						}
						break;

					case E_VALUE:
						if (this.state != Parser.E_ATTRIBUTE) Parser.log.warn("invalid element " + qName + " in " + this.state);
						this.state = Parser.E_VALUE;
						for (int i = 0; i < attrs.getLength(); i++)
							switch (attrs.getQName(i)) {
								case A_NAME:
									name = attrs.getValue(i);
									break;
								case A_DATA:
									data = Long.valueOf(attrs.getValue(i));
									break;
								default:
									Parser.log.warn("unknown attribute '" + attrs.getQName(i) + "' of value element");
							}
						if (name == null) throw new ParseException("no name attribute in value", 0);
						if (data == null) throw new ParseException("no data attribute in value", 0);
						if ((data.longValue() & 0x0FFFFFFFF) != data.longValue()) throw new ParseException("invalid value data=" + data, 0);
						if (this.currVendor != null && this.currAttrib != null) this.vendors.get(this.currVendor).attributes.get(
								this.currAttrib).addValue(name, data.longValue());
						break;

					default:
						throw new ParseException("unknown element '" + qName + "'", 0);
				}
			} catch (Exception e) {
				Parser.log.warn("error parsing element", e);
			}
		}

		/**
		 * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
		 */
		@Override
		public final void endElement(final String uri, final String localName, final String qName) throws SAXException {
			if (qName == null) throw new IllegalArgumentException("null qname");
			if (!qName.toLowerCase().equals(this.state)) Parser.log.warn("invalid end tag " + qName + " in " + this.state);
			switch (qName.toLowerCase()) {
				case E_DICTIONARY:
					this.state = Parser.E_DOCUMENT;
					break;
				case E_VENDOR:
					this.state = Parser.E_DICTIONARY;
					break;
				case E_ATTRIBUTE:
					this.state = Parser.E_VENDOR;
					break;
				case E_VALUE:
					this.state = Parser.E_ATTRIBUTE;
					break;
				default:
					Parser.log.warn("unknown end tag: " + qName);
			}
		}
	}
}
